/*
 * Copyright 2002-2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.jfast.framework.web.api;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.sql.Date;
import java.text.ParseException;
import java.text.SimpleDateFormat;

import cn.jfast.framework.base.SDKVersion;
import cn.jfast.framework.base.util.Assert;
import cn.jfast.framework.base.util.ReflectionUtils;
import cn.jfast.framework.base.util.StringUtils;
import cn.jfast.framework.log.LogFactory;
import cn.jfast.framework.log.LogType;
import cn.jfast.framework.log.Logger;
import cn.jfast.framework.web.api.annotation.*;
import cn.jfast.framework.upload.FileRequestResolver;
import cn.jfast.framework.upload.UploadFile;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * Api 对应实际方法的分析与执行
 */
public class ApiInvoker {

    private Logger log = LogFactory.getLogger(LogType.Jfast, ApiInvoker.class);
    /** 目标Api实际类 */
    private Class<?> targetClass;
    /** 目标Api实际类对象 */
    private Object targetObject;
    /** 目标Api执行方法 */
    private Method targetMethod;
    /** Api请求对象 */
    private ApiRequest request;
    /** HttpServletResponse */
    private HttpServletResponse response;
    /** Api方法对应参数类型数组 */
    private Type[] paramType;
    /** Api方法对应参数名数组*/
    private String[] paramNames;
    /** Api方法对应参数参数值 */
    private Object[] arguments = new Object[0];
    /** Api对应请求全路径*/
    private String route;
    /** 日期类型格式化对象 */
    private java.text.DateFormat dateFormat;
    /** JS分析引擎管理器 */
    private ScriptEngineManager sem;
    /** JS分析引擎 */
    private ScriptEngine se;

    public void setTargetClass(Class<?> targetClass) {
        this.targetClass = targetClass;
    }

    public void setTargetObject(Object targetObject) {
        this.targetObject = targetObject;
        if (targetObject != null) {
            this.targetClass = targetObject.getClass();
        }
    }

    public Object getTargetObject() {
        return this.targetObject;
    }

    public void setRoute(String route) {
        this.route = route;
    }

    public void setRequest(HttpServletRequest request) {
        this.request = new ApiRequest(request);
    }

    public void setResponse(HttpServletResponse response) {
        this.response = response;
    }

    public void setTargetMethod(Method targetMethod) {
        this.targetMethod = targetMethod;
    }

    /**
     * 获得请求参数对应的参数值
     * @return
     * @throws NotFoundException
     * @throws ParseException
     * @throws IllegalAccessException
     * @throws ScriptException
     * @throws InstantiationException
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public Object[] getArguments() throws NotFoundException, ParseException, IllegalAccessException, ScriptException, InstantiationException, IOException, ClassNotFoundException {
        ClassPool cp = ClassPool.getDefault();
        cp.insertClassPath(new ClassClassPath(ApiInvoker.class));
        CtClass clazz = cp.getCtClass(targetClass.getName());
        CtMethod method = clazz.getDeclaredMethod(targetMethod.getName());
        String paramName;
        paramType = targetMethod.getParameterTypes();
        paramNames = getMethodParamNames(method);
        arguments = new Object[paramNames.length];
        Object[][] annotations = method.getAvailableParameterAnnotations();
        for (int i = 0; i < paramNames.length; i++) {
            RequireParam reqParam = null;
            DateFormat df = null;
            RemoteObject ro = null;
            dateFormat = null;
            paramName = paramNames[i];
            for (Object anno : annotations[i]) {
                if (anno instanceof RequireParam) {
                    reqParam = (RequireParam) anno;
                } else if (anno instanceof DateFormat) {
                    df = (DateFormat) anno;
                    dateFormat = new SimpleDateFormat(df.format());
                } else if (anno instanceof RemoteObject) {
                    ro = (RemoteObject) anno;
                    if (targetMethod.isAnnotationPresent(Get.class)
                            || targetMethod.isAnnotationPresent(Delete.class)
                            || targetMethod.isAnnotationPresent(Head.class)
                            || targetMethod.isAnnotationPresent(Connect.class)
                            || targetMethod.isAnnotationPresent(Copy.class)
                            || targetMethod.isAnnotationPresent(Trace.class)
                            || targetMethod.isAnnotationPresent(Mkcol.class)
                            || targetMethod.isAnnotationPresent(Move.class)
                            || targetMethod.isAnnotationPresent(Unlock.class)
                            || targetMethod.isAnnotationPresent(Options.class)) {
                        throw new RuntimeException("Http请求方式为:" + request.getMethod() + "时,方法参数不可以使用RemoteObject注解," +
                                "RemoteObject只适用于PUT,POST,LOCK,PATCH,PROPFIND,PROPPATCH等请求方式," +
                                " 目标API路径 [ " + targetClass.getName() + "." + targetMethod.getName() + "() ]");
                    }
                }
            }
            if (null != ro && request.isRemoteRequest()) {
                if (request.getRequest().getInputStream().available() != 0) {
                    arguments[i] = new ObjectInputStream(request.getRequest().getInputStream()).readObject();
                }
            } else {
                if (paramType[i] == HttpServletRequest.class) {
                    arguments[i] = request.getRequest();
                } else if (paramType[i] == HttpServletResponse.class) {
                    arguments[i] = response;
                } else if (paramType[i] == HttpSession.class) {
                    arguments[i] = request.getRequest().getSession();
                } else if (paramType[i] == Cookie[].class) {
                    arguments[i] = request.getRequest().getCookies();
                } else {
                    if (null != reqParam && !reqParam.param().equals(""))
                        paramName = reqParam.param();
                    if (null != reqParam && reqParam.required())
                        Assert.notEmpty(request.getParameter(paramName),
                                String.format("请求参数 [%s] 不可以为空," +
                                        " 目标API路径为 [ " + targetClass.getName() + "." + targetMethod.getName() + "() ]", paramName));
                    arguments[i] = getMultiFieldValue(paramType[i], paramName, df,
                            dateFormat);
                }
            }
        }
        return this.arguments;
    }

    /**
     * 获取复杂类型对象参数值
     * @param type
     * @param paramName
     * @param df
     * @param dateFormat
     * @return
     * @throws IllegalAccessException
     * @throws ParseException
     * @throws InstantiationException
     * @throws ScriptException
     */
    private Object getMultiFieldValue(Type type, String paramName,
                                      DateFormat df, java.text.DateFormat dateFormat) throws IllegalAccessException, ParseException, InstantiationException, ScriptException {
        String tempParamName = paramName;
        Object target;
        if (((Class<?>) type).isPrimitive() || ((Class<?>) type).isArray()
                || type == String.class || type == Date.class
                || type == java.util.Date.class || type == UploadFile.class) {
            target = getSimpleFieldValue(type, paramName, df, dateFormat);
        } else {
            target = ((Class<?>) type).newInstance();
            //如果是JSON类型的参数
            if (StringUtils.isJson(request.getParameter(paramName))) {
                sem = new ScriptEngineManager();
                se = sem.getEngineByName("js");
                se.eval(String.format("var %s = eval(%s)",
                        paramName, request.getParameter(paramName)));
                for (Field field : ReflectionUtils
                        .getFields((Class<?>) type)) {
                    field.setAccessible(true);
                    type = field.getType();
                    Object value = getJsonFieldValue(type, field,
                            paramName, df, dateFormat);
                    field.set(target, value);
                }
            } else {
                for (Field field : ReflectionUtils
                        .getFields((Class<?>) type)) {
                    field.setAccessible(true);
                    paramName = tempParamName + "." + field.getName();
                    type = field.getType();
                    Object value = getSimpleFieldValue(type, paramName, df,
                            dateFormat);
                    if (null != value)
                        field.set(target, value);
                }
            }
        }

        return target;
    }

    /**
     * 获得Json类型参数对应的参数值
     * @param type
     * @param field
     * @param paramName
     * @param df
     * @param dateFormat
     * @return
     * @throws ScriptException
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws ParseException
     */
    private Object getJsonFieldValue(Type type, Field field, String paramName,
                                     DateFormat df, java.text.DateFormat dateFormat) throws ScriptException, IllegalAccessException, InstantiationException, ParseException {
        Object value;
        String fieldName = field.getName();
        se.eval(String.format("var %s = %s.%s", fieldName,
                paramName, fieldName));

        if (null == se.get(fieldName))
            return null;
        if (type == Integer.TYPE) {
            value = Integer.parseInt((String) se.get(fieldName));
        } else if (type == Short.TYPE) {
            value = Short.parseShort((String) se.get(fieldName));
        } else if (type == Float.TYPE) {
            value = Float.parseFloat((String) se.get(fieldName));
        } else if (type == Long.TYPE) {
            value = Long.parseLong((String) se.get(fieldName));
        } else if (type == Double.TYPE) {
            value = Double.parseDouble((String) se.get(fieldName));
        } else if (type == String.class) {
            value = se.get(fieldName);
        } else if (type == Date.class || type == java.util.Date.class) {
            Assert.notNull(df, "参数" + paramName + "必须声明DateFormat注解");
            value = dateFormat.parse((String) se.get(fieldName));
        } else if (((Class<?>) type).isArray() || type == Character.TYPE || type == Character[].class
                || type == Boolean.TYPE || type == Boolean[].class
                || type == Byte.TYPE || type == Byte[].class) {
            throw new IllegalArgumentException("不支持的参数类型:[" + type + "]");
        } else {
            value = ((Class<?>) type).newInstance();
            for (Field field2 : ReflectionUtils.getFields((Class<?>) type)) {
                field2.setAccessible(true);
                type = field2.getType();
                Object value2 = getJsonFieldValue(type, field2, paramName
                        + "." + fieldName, df, dateFormat);
                field2.set(value, value2);
            }
        }
        return value;
    }

    /**
     * 获得简单类型参数对应参数值
     * @param type
     * @param paramName
     * @param df
     * @param dateFormat
     * @return
     * @throws ParseException
     * @throws IllegalAccessException
     * @throws ScriptException
     * @throws InstantiationException
     */
    private Object getSimpleFieldValue(Type type, String paramName,
                                       DateFormat df, java.text.DateFormat dateFormat) throws ParseException, IllegalAccessException, ScriptException, InstantiationException {
        Object value = null;
        if (null != request.getParameter(paramName)
                || hasExtParam(paramName)
                || null != request.getUploadFile(paramName)) {
            String tempVal = request.getParameter(paramName);
            String tempVals[] = request.getParameters(paramName);
            if (type == Integer.TYPE) {
                value = Integer.parseInt(tempVal);
            } else if (type == Short.TYPE) {
                value = Short.parseShort(tempVal);
            } else if (type == Float.TYPE) {
                value = Float.parseFloat(tempVal);
            } else if (type == Long.TYPE) {
                value = Long.parseLong(tempVal);
            } else if (type == Double.TYPE) {
                value = Double.parseDouble(tempVal);
            } else if (type == String.class) {
                value = tempVal;
            } else if (type == UploadFile.class) {
                Assert.isTrue(
                        FileRequestResolver.isMultipart(request.getRequest()),
                        "上传文件时,表单属性enctype必须为multipart/form-data.");
                value = request.getUploadFile(paramName);
            } else if (type == Date.class || type == java.util.Date.class) {
                Assert.notNull(df, "参数" + paramName + "必须声明DateFormat注解");
                Assert.hasText(tempVal, "日期参数[" + paramName
                        + "]不可以为空");
                value = dateFormat.parse(tempVal);
            } else if (type == UploadFile[].class) {
                Assert.isTrue(
                        FileRequestResolver.isMultipart(request.getRequest()),
                        "上传文件时,表单属性enctype必须为multipart/form-data");
                value = request.getFileMap().values().toArray();
            } else if (type == Integer[].class || type == int[].class) {
                value = StringUtils.strArr2IntArr(tempVals);
            } else if (type == Short[].class || type == short[].class) {
                value = StringUtils.strArr2ShortArr(tempVals);
            } else if (type == Float[].class || type == float[].class) {
                value = StringUtils.strArr2FloatArr(tempVals);
            } else if (type == Long[].class || type == long[].class) {
                value = StringUtils.strArr2LongArr(tempVals);
            } else if (type == Double[].class || type == double[].class) {
                value = StringUtils.strArr2DoubleArr(tempVals);
            } else if (type == String[].class) {
                value = tempVals;
            } else if (type == Date[].class || type == java.util.Date[].class) {
                Assert.notNull(df, "参数" + paramName + "必须声明DateFormat注解");
                Assert.notEmpty(tempVals);
                value = StringUtils.strArr2DateArr(tempVals, dateFormat);
            } else if (type == Character.TYPE || type == Character[].class
                    || type == Boolean.TYPE || type == Boolean[].class
                    || type == Byte.TYPE || type == Byte[].class) {
                throw new IllegalArgumentException("不支持的参数类型:" + type);
            } else {
                value = getMultiFieldValue(type, paramName, df, dateFormat);
            }
        }
        return value;
    }

    /**
     * 判断是否有对应子参数名
     * @param paramName
     * @return
     */
    private boolean hasExtParam(String paramName) {
        boolean bool = false;
        for (String kv : request.getParameterNames()) {
            if (kv.indexOf(paramName) != -1) {
                bool = true;
                break;
            }
        }
        return bool;
    }

    /**
     * 获得方法参数名列表
     * @param cm
     * @return
     */
    protected static String[] getMethodParamNames(CtMethod cm) {
        MethodInfo methodInfo = cm.getMethodInfo();
        CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
        LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute
                .getAttribute(LocalVariableAttribute.tag);
        String[] paramNames = null;
        try {
            paramNames = new String[cm.getParameterTypes().length];
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
        int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;
        for (int i = 0; i < paramNames.length; i++) {
            paramNames[i] = attr.variableName(i + pos);
        }
        return paramNames;
    }

    /**
     * Api对应方法执行操作
     * @return
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws IllegalArgumentException
     * @throws NotFoundException
     * @throws ParseException
     * @throws ClassNotFoundException
     * @throws IOException
     * @throws InstantiationException
     * @throws ScriptException
     */
    public Object invoke() throws InvocationTargetException,
            IllegalAccessException, IllegalArgumentException,
            NotFoundException, ParseException, ClassNotFoundException, IOException, InstantiationException, ScriptException {

        getArguments();
        HttpServletRequest httpRequest = request.getRequest();
        Object targetObject = getTargetObject();
        ReflectionUtils.makeAccessible(targetMethod);

        dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        log.info("\n%s	:	------------------ [JFast SDK 版本-%s] ------------------\n" +
                        "%s	:	%s\n" +
                        "%s	:	%s\n" +
                        "%s		:	%s\n" +
                        "%s		:	%s\n" +
                        "%s	:	%s\n" +
                        "%s	:	%s\n",
                "接收请求", SDKVersion.version(),
                "请求地址", httpRequest.getRequestURI(),
                "请求方法", isAjax(httpRequest) ? httpRequest.getMethod()+"( ajax:"+httpRequest.getHeader("X-Requested-With")+" )" : httpRequest.getMethod(),
                "API别名", route,
                "API路径", targetObject.getClass().getName() + "." + targetMethod.getName() + "()",
                "API参数名", "[" + StringUtils.join(paramNames, ",") + "]",
                "API参数值", "[" + StringUtils.join(arguments, ",") + "]");

        Object result = targetMethod.invoke(targetObject, arguments);
        log.info("\n%s	:	------------------ [JFast SDK 版本-%s] ------------------\n" +
                        "%s	:	%s\n" +
                        "%s	:	%s\n",
                "请求返回", SDKVersion.version(),
                "目标地址", getIpAddr(httpRequest) + ":" + httpRequest.getRemotePort(),
                "返回视图", null == result ? "" : result.toString());
        return result;
    }

    /**
     * 获得远程IP地址
     * @param request
     * @return
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    /**
     * 获得Ajax请求描述
     * @param request
     * @return
     */
    public static boolean isAjax(HttpServletRequest request) {
        return null != request.getHeader("X-Requested-With");
    }


}
